<?php 
/**
 * @package scottlib.bigtables
 * @author Aaron Collegeman aaroncollegeman.com
 * @version 1.0.0-BETA
 * 
 * Copyright (C)2009 Collegeman.net, LLC.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 */

///////////////////////////////////////////////////////////////////////////////
// This file defines the interface for bigtable engines and implementations.
///////////////////////////////////////////////////////////////////////////////

if (!defined('SL_BIGTABLES_NUMERIC_MIN')) define('SL_BIGTABLES_NUMERIC_MIN', -1073741823);

if (!defined('SL_BIGTABLES_NUMERIC_MAX')) define('SL_BIGTABLES_NUMERIC_MAX',  2147483647 + SL_BIGTABLES_NUMERIC_MIN);

if (!defined('SL_BIGTABLES_DECIMAL_PRECISION')) define('SL_BIGTABLES_DECIMAL_PRECISION', 2);

if (!defined('SL_BIGTABLES_DATE_FORMAT')) define('SL_BIGTABLES_DATE_FORMAT', 'c'); // ISO 8601 date, e.g., 2004-02-12T15:19:21+00:00

if (!defined('SL_BIGTABLES_NAMESPACE')) define('SL_BIGTABLES_NAMESPACE', 'sl_bigtables_');

/**
 * Base class for wrapping connections to structured data systems.
 * 
 * This class is named after Google's Bigtable - their own structured data system - but was primarily
 * developed to simplify the use of Amazon's SimpleDB via the Tarzan AWS library.
 * 
 * Also, another primary goal is to use MySql to immitate SimpleDB, therein allowing for a unified
 * data storage API across cloud and private platforms.
 * 
 * @since 1.0.0-BETA
 */
abstract class slBigtableEngine {
	
	protected static $instance;
	
	private $numericMin = SL_BIGTABLES_NUMERIC_MIN;
	
	private $numericMax = SL_BIGTABLES_NUMERIC_MAX;
	
	private $decimalPrecision = SL_BIGTABLES_DECIMAL_PRECISION;
	
	private $dateFormat = SL_BIGTABLES_DATE_FORMAT; 
	
	private static $engineClasses = array('slSimpleDBBigtableEngine', 'slMySqlBigtableEngine');
	
	static final function registerEngine($className) {
		if (!in_array($className, self::$engineClasses))
			self::$engineClasses[] = $className;
	}
	
	static final function getRegisteredEngines() {
		return self::$engineClasses;
	}
	
	/**
	 * @param $path Load a specific item or attribute
	 */
	abstract function get($path, $keys = null);
	
	/**
	 * @param $args slData A data object to be persisted
	 */
	abstract function save(slData $data);
	
	/**
	 * @param $args Array An array of mixed elements - slData objects or Strings, Strings
	 * representing item - to be removed from persistence
	 */
	abstract function delete($domain, $item = null, $keys = null);

	abstract function select($domainOrExpressionOrCriteria, $nextToken = null);
	
	abstract function create_domain($name = null);
	
	abstract function domain_exists($name = null);
	
	abstract function drop_domain($name = null);
	
	abstract function list_domains();
	
	abstract function next_token();
	
	abstract function meta($domain_name);
	
	final function instance() {
		if (!self::$instance)
			throw new Exception('No bigtable engine has yet been connected.');
			
		return self::$instance;
	}
	
	final function setNumericRange($min, $max) {
		$this->numericMin = $min;
		$this->numericMax = $max;
	}
	
	final function range($min, $max) {
		$this->setNumericRange($min, $max);
	}
	
	final function setDecimalPrecision($precision) {
		$this->decimalPrecision = $precision;
	}
	
	final function decimal($precision) {
		$this->setDecimalPrecision($precision);
	}
	
	final function setDateFormat($format) {
		if (date($format) === false)
			throw new Exception("[$format] is not a valid date format");
		$this->dateFormat($format);	
	}
	
	final function date($format) {
		$this->setDateFormat($format);
	}
	
	protected final function namespace($name) {
		return SL_BIGTABLES_NAMESPACE . $name;
	}
	
	/**
	 * Formats the given value according to its perceived type, allowing for it to be sorted as text.
	 * @param $str mixed
	 */
	 final function pad($str) {
		if (($time = strtotime($str)) !== false) {
			return date($this->dateFormat, $time);
		}
		
		else if (is_float($str) || is_int($str)) {
			$val = $str;
			
			if (is_float($val))
				$val = round($str, $this->decimalPrecision);
				
			if ($val > $this->numericMax)
				throw new Exception("Value [$val] exceeds numeric maximum [$this->numericMax]");
				
			$val += abs($this->numericMin);

			$parts = split("\.", $val);
			$int = $parts[0];
			$dec = count($parts) > 1 ? $parts[1] : null;
			
			for ($i=0; $i<strlen($this->numericMax) - strlen($int); $i++)
				$int = "0".$int;
				
			if ($dec) {
				for ($i=0; $i<strlen($this->decimalPrecision) - strlen($dec); $i++)
					$dec .= "0";
			}
			
			return ($dec ? "$int.$dec" : $int);	
		}
		
		else if (is_numeric($str)) {
			throw new Exception("What kind of numeric is this? $numeric");
		}
		
		else {
			// do nada
			return $str;
		}
	}
	
	/**
	 * Unformats the given value according to its perceived type.
	 * @param $str mixed
	 */
	final function unpad($str) {
		if (is_numeric($str)) {
			return round($str-abs($this->numericMin), $this->decimalPrecision);
		}
		else {
			// do nada
			return $str;
		}
	}
	
	protected final function parsePath($path, $keys = array()) {
		$key = null;
		
		if (strpos($path, '->')) {
			if (strpos($path, '->') !== strrpos($path, '->')) // domain->item->key
				list($domain, $item, $key) = split("\-\>", $path);
			else
				list($domain, $item) = split("\-\>", $path);
		}
		else {
			$domain = 'default';
			$item = $path;
		}
		
		if ($key)
			$keys[] = $key;
			
		return array($domain, $item, $keys);
	}
	
	protected final function parseDeleteArgs($args) {
		if (count($args) == 1 && is_array($args[0])) {
			throw new Exception("Parameters to delete() method must include either {slData instance}, {itemName}, {domain, itemName}, {itemName, keys()}, or {domain, itemName, keys()}");
		}
		
		if (count($args) == 1) {
			if ($args[0] instanceof slData)
				return array($args[0]->domain(), $args[0]->itemName, null);
			else
				return array('default', $args[0], null);
		}
		
		else if (count($args) == 2) {
			return (is_array($args[1])) ? array('default', $args[0], $args[1]) : array($args[0], $args[1], null);
		}
		
		else if (count($args) >= 3) {
			return array($args[0], $args[1], $args[2]);
		}
	}
	
}

/**
 * An implementation of the slBigtableEngine API for Amazon's SimpleDB.
 * 
 * This implementation depends on Tarzan 2.0.4+.  If not already available at
 * run-time, the Tarzan file "tarzan.class.php" will be imported by this
 * class' constructor.
 * 
 * @see http://tarzan-aws.com
 * @since 1.0.0-BETA
 */
final class slSimpleDBBigtableEngine extends slBigtableEngine {
	
	private $sdb;
	
	private $debug = false;
	
	function __construct($args) {
		if (!class_exists('AmazonSDB'))
			require('lib/tarzan_2.0.4/tarzan.class.php');
		
		$args = is_array($args) ? $args : func_get_args();
		
		$key = func_parse_args($args, 'key', 0, true);
		$secret_key = func_parse_args($args, 'secret_key', 1, true);
		$this->debug = func_parse_args($args, 'debug', 2, false, false); 
		
		$this->sdb = new AmazonSDB($key, $secret_key);
		
		slBigtableEngine::$instance = $this;
	}
	
	private function isOK(TarzanHTTPResponse $response) {
		if (!$response->isOK()) {
			$status = $response->status;
			$error = $response->body->Errors->Error;
			$code = $error->Code;
			$message = $error->Message;
			throw new slBigtableEngineException("$status $code: $message");
		}
		else
			return true;
	}
	
	final function get($path, $keys = null) {
		list($domain, $item, $keys) = $this->parsePath($path, $keys);
		
		if ($this->isOK($response = $this->sdb->get_attributes($this->namespace($domain), $item, $keys))) {
			
			if (!$response->body->GetAttributesResult->Attribute) {
				return null; 
			}
			else {
				$data = new slData($domain, $item, null, $this);
				
				foreach($response->body->GetAttributesResult->Attribute as $attribute) {
					$name = $attribute->Name;
					$data->$name = "$attribute->Value";
				}
				
				return $data;
			}

		}
	}
	
	final function create_domain($name = null) {
		if (!$name)
			$name = 'default';
		$this->isOK($this->sdb->create_domain($this->namespace($name)));
	}
	
	final function domain_exists($name = null) {
		if (!$name)
			$name = 'default';
		return in_array($this->namespace($name), $this->list_domains());
	}
	
	final function drop_domain($name = null) {
		if (!$name)
			$name = 'default';
		$this->isOK($this->sdb->delete_domain($this->namespace($name)));
	}
	
	final function list_domains() {
		$domains = array();
		if ($this->isOK($list = $this->sdb->list_domains())) {
			if ($list->body->ListDomainsResult->DomainName) {
				foreach($list->body->ListDomainsResult->DomainName as $name)
					$domains[] = "$name";	
			}
			return $domains;
		}
	}
	
	final function meta($domain_name) {
		// TODO: create a scottlib wrapper for meta data
		if ($this->isOK($response = $this->sdb->domain_metadata($domain_name)))
			return $response; 	
	}
	
	final function save(slData $data) {
		
		$data_arr = $data->data();
		foreach($data_arr as $key => $value) {
			if (is_array($value)) {
				foreach($value as $i => $va) {
					if ($va instanceof slSerialized)
						$data_arr[$key][$i] = $va->serial();
				}
			}
			else {
				if ($value instanceof slSerialized)
					$data_arr[$key] = $value->serial();
			}
			
		}
		
		$this->isOK($this->sdb->put_attributes($this->namespace($data->domain()), $data->item(), $data_arr, (count($data->replacements()) ? $data->replacements() : false)));

		return $data->item();
		
	}
	
	final function delete($domain, $item = null, $keys = null) {
		list ($domain, $item, $keys) = $this->parseDeleteArgs(func_get_args());
		$this->isOK($response = $this->sdb->delete_attributes($this->namespace($domain), $item, $keys));
	}
	
	private function selectWithCriteria($domain, slCriteria $criteria) {
		$builder = new slSimpleDBCriteriaBuilder($domain, $args);
		$query = $builder->getString();
		
		$options = null;
		if ($nextToken = $builder->getFirstResult()) {
			$options = array('NextToken' => $nextToken);
		}
		
		$this->isOK($response = $this->sdb->select($query, $options));
		
		return $response;
	}
	
	final function select($domainOrExpressionOrCriteria, $nextToken = null) {
		
		$args = array($domainOrExpressionOrCriteria, $nextToken);
		
		$response = null;
		
		// first arg is slCriteria
		if (is_assignable_from($args[0], 'slCriteria')) {
			$domain = $this->namespace('default');
			$response = $this->selectWithCriteria($domain, $args[0]);
		}
		
		// first arg is domain, second arg is slCriteria
		else if (is_string($args[0]) && is_assignable_from($args[1], 'slCriteria')) {
			$response = $this->selectWithCriteria($this->namespace($args[0]), $args[1]);
		}
		
		// first arg is domain, second arg is expression
		else if (is_string($args[0]) && (is_string($args[1]) || $args[1] == null)) {
			$options = ($args[1] != null ? array('NextToken' => $args[1]) : null);
			$this->isOK($response = $this->sdb->select($args[1], $options));
		}
		
		else {
			throw new slBigtableEngineException("TODO: write informative exception for misuse of slSimpleDBBigtableEngine::select method");
		}
		
		// turn response into slData objects
		
	}
	
	final function next_token() {
		
	}

	
}


/**
 * An implementation of the slBigtableEngine API for MySQL.
 * 
 * @since 1.0.0-BETA
 */
final class slMySqlBigtableEngine extends slBigtableEngine {
	
	private $conn;
	
	function __construct($args) {
		$args = is_array($args) ? $args : func_get_args();
		$this->conn = new slMySqlConn($args);
		$this->conn->connect();
		slBigtableEngine::$instance = $this;
	}
	
	final function get($path, $keys = null) {
		
	}
	
	final function create_domain($name = null) {
		
	}
	
	final function domain_exists($name = null) {
		
	}
	
	final function drop_domain($name = null) {
		
	}
	
	final function list_domains() {
		
	}
	
	final function meta($domain_name) {
		
	}
	
	final function save(slData $data) {
		
	}
	
	final function delete($domain, $item = null, $keys = null) {
		
	}
	
	final function select($domainOrExpressionOrCriteria, $nextToken = null) {
		
	}
	
	final function next_token() {
		
	}
	
}

/**
 * Whenever possible, bigtable-related exceptions should be wrapped in
 * this marker class.
 */
final class slBigtableEngineException extends Exception {
	
	function __construct($message, $cause = null) {
		parent::__construct($message, $cause);
	}
	
}

///////////////////////////////////////////////////////////////////////////////
// Function definitions
///////////////////////////////////////////////////////////////////////////////

if (!function_exists("bigtable")) {
	function bigtable($args = null) {
		$args = is_array($args) ? $args : func_get_args();
		
		// no parameter invocation: return most recent instance
		if (!count($args))
			return slBigtableEngine::instance();
		
		// scan arguments for an engine
		$engine = null;
		$registeredEngines = slBigtableEngine::getRegisteredEngines();
		foreach($args as $arg) {
			if (is_string($arg) && in_array($arg, $registeredEngines)) {
				$engine = $arg;
				break;
			}
		}
		
		
		// default engine is SimpleDB
		if (!$engine)
			$engine = 'slSimpleDBBigtableEngine';
			
		return new $engine($args);
	}
}

if (!function_exists("bigtable_next_token")) {
	function bigtable_next_token() {
		return bigtable()->nextToken();	
	}
}

if (!function_exists("data_save")) {
	function data_save($domain, $item = null, $data = null, $engine = null) {
		$args = func_get_args();
		
		///////////////////////////////////////////////////////////////////////
		// There are six valid arg configurations
		///////////////////////////////////////////////////////////////////////
		
		if (count($args) == 1) {
			
			// arg cfg #1: data
			// 'default' domain, the most recent bigtable engine instance, and a new id
			
			$domain = 'default';
			$item = sl_guid();
			$data = $args[0];
			$engine = slBigtableEngine::instance();
		}
		
		else if (count($args) == 2) {
			$domain = 'default';
			
			if (is_array($args[0])) {
				
				// arg cfg #2: data, engine
				// 'default' domain and a new id

				$item = sl_guid();
				$data = $args[0];
				$engine = $args[1];
			}
			
			else {
				
				// arg cfg #3: item, data
				// 'default' domain, explicit item, the most recent bigtable engine instance 
				
				$item = $args[0];
				$data = $args[1];
				$engine = slBigtableEngine::instance();
				
			}
		}
		
		else if (count($args) == 3) {
			$domain = $args[0];
			
			if ($args[2] instanceof slBigtableEngine) {
				
				// arg cfg #4: domain, data, engine
				// new id
				
				$data = $args[1];
				$engine = $args[2];
			}
			else {
				
				// arg cfg #5: domain, item, data
				// the most recent bigtable engine instance
				
				$item = $args[1];
				$data = $args[2];
				$engine = slBigtableEngine::instance();
			}
		}
		
		else if (count($args) == 4) {
			
			// arg cfg #6: domain, item, data, engine
			// everything explicit
			
			list ($domain, $item, $data, $engine) = $args;
		}
		
		$data = new slData($domain, $item, $data, $engine);
		$data->save();
		return $data;
	}
}